-
Notifications
You must be signed in to change notification settings - Fork 50
Complex DK jump sequences bug fix and extended testing #2159
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
❌ Deploy Preview for kleros-v2-testnet-devtools failed. Why did it fail? →
|
WalkthroughReplaces per-dispute boolean active flag with an Active struct (dispute, currentRound), reworks createDispute to reuse active local disputes or create new ones, updates modifiers to isActive, changes withdrawal/event signatures, and adjusts tests to the new active/currentRound and withdrawal semantics. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Core as KlerosCore
participant DK as DisputeKitClassicBase
participant Store as Storage (mappings)
Core->>DK: createDispute(coreDisputeID,...)
DK->>Store: read coreDisputeIDToActive[coreDisputeID]
alt Active.dispute == true
DK->>Store: localID = coreDisputeIDToLocal[coreDisputeID]
DK->>DK: reuse local dispute, set Active.currentRound = true
DK->>Store: coreRoundIDToLocal[newCoreRoundID] = dispute.rounds.length (store-length)
DK-->>Core: return localID
else Active.dispute == false
DK->>DK: create new local dispute & round
DK->>Store: coreDisputeIDToLocal[coreDisputeID] = newLocalID
DK->>Store: coreDisputeIDToActive[coreDisputeID] = Active(true,true)
DK->>Store: coreRoundIDToLocal[newCoreRoundID] = dispute.rounds.length
DK-->>Core: return newLocalID
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
✅ Deploy Preview for kleros-v2-testnet ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for kleros-v2-neo ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (4)
contracts/test/foundry/KlerosCore_Appeals.t.sol (4)
609-619
: Pin event emitters to avoid false positivesTie expected logs to their emitters to ensure they come from the intended contracts.
Apply this diff:
- vm.expectEmit(true, true, true, true); + vm.expectEmit(address(core)); emit KlerosCore.CourtJump(disputeID, 1, courtID3, courtID2); - vm.expectEmit(true, true, true, true); + vm.expectEmit(address(core)); emit KlerosCore.DisputeKitJump(disputeID, 1, dkID3, dkID2); - vm.expectEmit(true, true, true, true); + vm.expectEmit(address(disputeKit2)); emit DisputeKitClassicBase.DisputeCreation(disputeID, 2, newExtraData); - vm.expectEmit(true, true, true, true); + vm.expectEmit(address(core)); emit KlerosCore.AppealDecision(disputeID, arbitrable); - vm.expectEmit(true, true, true, true); + vm.expectEmit(address(core)); emit KlerosCore.NewPeriod(disputeID, KlerosCore.Period.evidence);
685-691
: Also pin emitters on the second jump (and consider asserting NewPeriod)Use specific emitters here as well; optionally assert NewPeriod(evidence) like in the first jump.
- vm.expectEmit(true, true, true, true); + vm.expectEmit(address(core)); emit KlerosCore.CourtJump(disputeID, 2, courtID2, GENERAL_COURT); - vm.expectEmit(true, true, true, true); + vm.expectEmit(address(core)); emit KlerosCore.DisputeKitJump(disputeID, 2, dkID2, dkID3); + vm.expectEmit(address(core)); + emit KlerosCore.NewPeriod(disputeID, KlerosCore.Period.evidence);
605-606
: Avoid magic funding amounts; compute from appealCost and multipliersHard-coded values (0.63, 0.42, 1.35, 0.9 ether) will drift if fees/multipliers change. Derive from core.appealCost(disputeID).
Example:
- disputeKit3.fundAppeal{value: 0.63 ether}(disputeID, 1); + uint256 ac1 = core.appealCost(disputeID); + uint256 lose1 = ac1 + (ac1 * 20000) / Constants.MULTIPLIER_DIVISOR; + disputeKit3.fundAppeal{value: lose1}(disputeID, 1); ... - disputeKit3.fundAppeal{value: 0.42 ether}(disputeID, 2); + uint256 win1 = ac1 + (ac1 * 10000) / Constants.MULTIPLIER_DIVISOR; + disputeKit3.fundAppeal{value: win1}(disputeID, 2); ... - disputeKit2.fundAppeal{value: 1.35 ether}(disputeID, 1); + uint256 ac2 = core.appealCost(disputeID); + uint256 lose2 = ac2 + (ac2 * 20000) / Constants.MULTIPLIER_DIVISOR; + disputeKit2.fundAppeal{value: lose2}(disputeID, 1); ... - disputeKit2.fundAppeal{value: 0.9 ether}(disputeID, 2); + uint256 win2 = ac2 + (ac2 * 10000) / Constants.MULTIPLIER_DIVISOR; + disputeKit2.fundAppeal{value: win2}(disputeID, 2);Note: If multipliers are accessible via getters, prefer them over literals.
Also applies to: 620-621, 683-684, 690-691
758-766
: Optional: assert total rounds after the second jumpQuick sanity on core-side tracking.
core.executeRuling(disputeID); // winning choice is 2 + assertEq(core.getNumberOfRounds(disputeID), 3, "Should have 3 rounds after two appeals");
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
contracts/test/foundry/KlerosCore_Appeals.t.sol
(1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
- GitHub Check: Redirect rules - kleros-v2-testnet
- GitHub Check: Redirect rules - kleros-v2-testnet-devtools
- GitHub Check: Redirect rules - kleros-v2-testnet
- GitHub Check: Header rules - kleros-v2-testnet-devtools
- GitHub Check: Header rules - kleros-v2-testnet
- GitHub Check: Redirect rules - kleros-v2-neo
- GitHub Check: Header rules - kleros-v2-testnet
- GitHub Check: Pages changed - kleros-v2-testnet-devtools
- GitHub Check: Header rules - kleros-v2-neo
- GitHub Check: Pages changed - kleros-v2-testnet
- GitHub Check: Pages changed - kleros-v2-testnet
- GitHub Check: Pages changed - kleros-v2-neo
- GitHub Check: Analyze (javascript)
- GitHub Check: hardhat-tests
🔇 Additional comments (1)
contracts/test/foundry/KlerosCore_Appeals.t.sol (1)
470-793
: Thorough recurring DK/court jump scenario — looks solidCovers re-entry semantics, round mapping, jump gating, events, and withdrawals end-to-end. No blockers.
3d197d6
to
1e7d185
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol (1)
141-144
: notJumped can revert with storage OOB for unknown disputesIf called with a
_coreDisputeID
this DK never created,coreDisputeIDToLocal[_coreDisputeID]
is 0 anddisputes[0]
may be OOB (length==0). Prefer checking activity first to provide a deterministic custom error instead of a panic.- modifier notJumped(uint256 _coreDisputeID) { - if (disputes[coreDisputeIDToLocal[_coreDisputeID]].jumped) revert DisputeJumpedToParentDK(); + modifier notJumped(uint256 _coreDisputeID) { + if (!coreDisputeIDToActive[_coreDisputeID]) revert NotActiveForCoreDisputeID(); + uint256 local = coreDisputeIDToLocal[_coreDisputeID]; + if (disputes[local].jumped) revert DisputeJumpedToParentDK(); _; }
🧹 Nitpick comments (4)
contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol (3)
209-221
: Recurring DK reuse path looks good; consider idempotent active-flag setReusing the existing localDisputeID and resetting jumped=false is correct for returning disputes. Optionally reassert
coreDisputeIDToActive[_coreDisputeID] = true;
in both branches to be explicit/idempotent.if (coreDisputeIDToActive[_coreDisputeID]) { localDisputeID = coreDisputeIDToLocal[_coreDisputeID]; dispute = disputes[localDisputeID]; dispute.jumped = false; + coreDisputeIDToActive[_coreDisputeID] = true; // idempotent } else { localDisputeID = disputes.length; dispute = disputes.push(); coreDisputeIDToLocal[_coreDisputeID] = localDisputeID; coreDisputeIDToActive[_coreDisputeID] = true; }
229-231
: Guard against accidental underflow and cache the round IDMinor: cache the core round ID and assert it exists before subtracting. Improves readability and prevents a panic if call ordering ever changes.
- // New round in the Core should be created before the dispute creation in DK. - dispute.coreRoundIDToLocal[core.getNumberOfRounds(_coreDisputeID) - 1] = dispute.rounds.length; + // New round in the Core should be created before the dispute creation in DK. + uint256 coreRoundID = core.getNumberOfRounds(_coreDisputeID); + require(coreRoundID > 0, "Core round not initialized"); + dispute.coreRoundIDToLocal[coreRoundID - 1] = dispute.rounds.length; // 1-based storage
462-463
: Avoid -1 underflow panics; centralize local round lookupAll these sites assume
coreRoundIDToLocal[_coreRoundID] != 0
and do- 1
. If unset, you get a panic (arithmetic or OOB). Tests currently rely on panics to detect “wrong DK/round”, but a targeted error improves debuggability and future flexibility.Suggestion:
- Add an internal helper returning the 0-based localRound index with a descriptive revert.
- Use it across read paths to remove duplication and accidental off-by-one errors.
// Add once: error RoundNotHandledByThisDK(); function _localRoundIndexOrRevert(Dispute storage d, uint256 _coreRoundID) internal view returns (uint256) { uint256 idx1 = d.coreRoundIDToLocal[_coreRoundID]; if (idx1 == 0) revert RoundNotHandledByThisDK(); // not mapped in this DK return idx1 - 1; // convert to 0-based }Then replace patterns like:
Round storage round = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID] - 1]; Vote storage vote = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID] - 1].votes[_voteID]; localRoundID = disputes[localDisputeID].coreRoundIDToLocal[_coreRoundID] - 1;with:
uint256 lr = _localRoundIndexOrRevert(dispute, _coreRoundID); Round storage round = dispute.rounds[lr]; Vote storage vote = dispute.rounds[lr].votes[_voteID]; localRoundID = _localRoundIndexOrRevert(disputes[localDisputeID], _coreRoundID);Note: Keeping current tests is possible by also testing for
RoundNotHandledByThisDK()
in addition to panics.Also applies to: 566-567, 579-580, 647-648, 670-671, 698-699, 708-709
contracts/test/foundry/KlerosCore_Appeals.t.sol (1)
471-800
: Strong E2E coverage for recurring DK; consider reducing brittleness and duplication
- Reliance on
stdError.arithmeticError
is brittle; a future switch to explicit custom errors would break tests. Consider accepting both via try/catch or dedicated helpers.- Extract small helpers for repeated steps (stake/draw/vote/fund/expectJump) to cut boilerplate and improve readability.
If you want, I can propose a small set of reusable helpers for these flows.
Please confirm if we should keep asserting for panics vs. migrate to explicit error types in DK.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol
(10 hunks)contracts/test/foundry/KlerosCore_Appeals.t.sol
(2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
- GitHub Check: Redirect rules - kleros-v2-neo
- GitHub Check: Redirect rules - kleros-v2-testnet-devtools
- GitHub Check: Header rules - kleros-v2-neo
- GitHub Check: Header rules - kleros-v2-testnet-devtools
- GitHub Check: Pages changed - kleros-v2-neo
- GitHub Check: Pages changed - kleros-v2-testnet-devtools
- GitHub Check: Redirect rules - kleros-v2-testnet
- GitHub Check: Header rules - kleros-v2-testnet
- GitHub Check: Redirect rules - kleros-v2-testnet
- GitHub Check: Header rules - kleros-v2-testnet
- GitHub Check: Pages changed - kleros-v2-testnet
- GitHub Check: Pages changed - kleros-v2-testnet
- GitHub Check: hardhat-tests
- GitHub Check: Analyze (javascript)
🔇 Additional comments (2)
contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol (2)
28-28
: Clarify 1-based index semantics in mappingStoring the length (1-based) is fine and matches the new read-path offsets. Please keep this invariant documented prominently, as misuse will cause subtle -1 underflows elsewhere.
432-436
: Indexing for the next round on no-jump path is correctSetting
coreRoundIDToLocal[coreRoundID + 1] = dispute.rounds.length + 1
before pushing the new round matches the 1-based scheme. Good.
…Rewards(), DK errors renamed
1e7d185
to
536e61c
Compare
Refactor the Dispute Kit checks: active dispute and isJumped
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol (1)
105-118
: Reorder to check activity/ownership before touching local round; avoid underflow and wasted writes.If the dispute is not active in this DK,
dispute.rounds.length - 1
can underflow or you perform writes that will be reverted anyway. Call_castCommit
first (it enforces active+ownership), then computelocalRoundID
and writerecoveryCommitments
.- ) external { - if (_recoveryCommit == bytes32(0)) revert EmptyRecoveryCommit(); - - uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID]; - Dispute storage dispute = disputes[localDisputeID]; - uint256 localRoundID = dispute.rounds.length - 1; - for (uint256 i = 0; i < _voteIDs.length; i++) { - recoveryCommitments[localDisputeID][localRoundID][_voteIDs[i]] = _recoveryCommit; - } - - // `_castCommit()` ensures that the caller owns the vote and that dispute is active - _castCommit(_coreDisputeID, _voteIDs, _commit); - emit CommitCastShutter(_coreDisputeID, msg.sender, _commit, _recoveryCommit, _identity, _encryptedVote); - } + ) external { + if (_recoveryCommit == bytes32(0)) revert EmptyRecoveryCommit(); + // `_castCommit()` ensures the caller owns the votes and the dispute is active in this DK. + _castCommit(_coreDisputeID, _voteIDs, _commit); + // Safe to touch local dispute/round after validation. + uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID]; + Dispute storage dispute = disputes[localDisputeID]; + uint256 localRoundID = dispute.rounds.length - 1; + for (uint256 i = 0; i < _voteIDs.length; i++) { + recoveryCommitments[localDisputeID][localRoundID][_voteIDs[i]] = _recoveryCommit; + } + emit CommitCastShutter(_coreDisputeID, msg.sender, _commit, _recoveryCommit, _identity, _encryptedVote); + }contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol (1)
121-134
: Same reorder needed as in Shutter: validate via _castCommit before computing localRoundID/writing.Prevents arithmetic underflow and redundant writes in reverted paths.
- ) external { - if (_recoveryCommit == bytes32(0)) revert EmptyRecoveryCommit(); - - uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID]; - Dispute storage dispute = disputes[localDisputeID]; - uint256 localRoundID = dispute.rounds.length - 1; - for (uint256 i = 0; i < _voteIDs.length; i++) { - recoveryCommitments[localDisputeID][localRoundID][_voteIDs[i]] = _recoveryCommit; - } - - // `_castCommit()` ensures that the caller owns the vote and that dispute is active - _castCommit(_coreDisputeID, _voteIDs, _commit); - emit CommitCastShutter(_coreDisputeID, msg.sender, _commit, _recoveryCommit, _identity, _encryptedVote); - } + ) external { + if (_recoveryCommit == bytes32(0)) revert EmptyRecoveryCommit(); + // Validate ownership + active state first. + _castCommit(_coreDisputeID, _voteIDs, _commit); + // Then write per-round recovery commitments. + uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID]; + Dispute storage dispute = disputes[localDisputeID]; + uint256 localRoundID = dispute.rounds.length - 1; + for (uint256 i = 0; i < _voteIDs.length; i++) { + recoveryCommitments[localDisputeID][localRoundID][_voteIDs[i]] = _recoveryCommit; + } + emit CommitCastShutter(_coreDisputeID, msg.sender, _commit, _recoveryCommit, _identity, _encryptedVote); + }contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol (1)
56-75
: Upgrade risk: changing mapping value type breaks live gating semanticsSwitching coreDisputeIDToActive from mapping(uint256=>bool) to mapping(uint256=>Active) preserves the first field (dispute) but initializes currentRound to false for all pre-existing disputes. After upgrade, isActive will revert (DisputeJumpedToAnotherDisputeKit) for ongoing rounds until a fresh createDispute call sets currentRound=true, potentially bricking live disputes.
Options:
- Transitional fallback in isActive: treat currentRound==false as true if dispute==true and the dispute’s latest local round maps to core’s current round.
- Add a one-time post-upgrade init function (guarded, non-reentrant) to set currentRound=true for currently active disputes (requires iterability or tracked keys).
- Maintain the old boolean mapping (e.g., coreDisputeIDToActiveLegacy) and default currentRound = legacy value on first access/mutation.
Minimal code change alternative (fallback in isActive):
- modifier isActive(uint256 _coreDisputeID) { - if (!coreDisputeIDToActive[_coreDisputeID].dispute) revert DisputeUnknownInThisDisputeKit(); - if (!coreDisputeIDToActive[_coreDisputeID].currentRound) revert DisputeJumpedToAnotherDisputeKit(); + modifier isActive(uint256 _coreDisputeID) { + Active storage a = coreDisputeIDToActive[_coreDisputeID]; + if (!a.dispute) revert DisputeUnknownInThisDisputeKit(); + if (!a.currentRound) { + // Fallback for pre-upgrade disputes: consider active if local mapping matches core's current round. + uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID]; + if (disputes.length == 0) revert DisputeJumpedToAnotherDisputeKit(); + uint256 coreRoundID = core.getNumberOfRounds(_coreDisputeID) - 1; + bool matches = (disputes[localDisputeID].coreRoundIDToLocal[coreRoundID] == (disputes[localDisputeID].rounds.length == 0 ? type(uint256).max : disputes[localDisputeID].rounds.length - 1)); + if (!matches) revert DisputeJumpedToAnotherDisputeKit(); + a.currentRound = true; // self-heal + } _; }Document the migration in release notes. Based on learnings
🧹 Nitpick comments (9)
contracts/src/arbitration/interfaces/IDisputeKit.sol (1)
33-45
: Clarify the precondition and call ordering for createDispute.Good note. Recommend tightening the NatSpec to state the ordering and expected behavior on violation.
/// @notice Creates a local dispute and maps it to the dispute ID in the Core contract. /// @dev Access restricted to Kleros Core only. - /// @dev The new `KlerosCore.Round` must be created before calling this function. + /// @dev Requirements: + /// - KlerosCore MUST have created the next Round for `_coreDisputeID` before calling this function. + /// - Implementations SHOULD revert if the round does not exist or the dispute is not active in this DK. + /// - Implementations SHOULD reuse an existing local dispute if already active (recurring DK scenarios). /// @param _coreDisputeID The ID of the dispute in Kleros Core, not in the Dispute Kit.contracts/test/foundry/KlerosCore_Disputes.t.sol (1)
95-98
: Minor: use disputeID variable and clearer tuple names.Improves readability and avoids hardcoded IDs.
- (bool dispute, bool currentRound) = disputeKit.coreDisputeIDToActive(0); - assertEq(dispute, true, "Dispute should be active in this DK"); - assertEq(currentRound, true, "Current round should be active in this DK"); + (bool disputeActive, bool currentRoundActive) = disputeKit.coreDisputeIDToActive(disputeID); + assertEq(disputeActive, true, "Dispute should be active in this DK"); + assertEq(currentRoundActive, true, "Current round should be active in this DK");contracts/test/foundry/KlerosCore_Execution.t.sol (1)
724-724
: LGTM: withdrawal API and event updates reflected. Add idempotency check (optional).The calls and event args match the simplified signature. Optionally, assert a second withdrawal reverts or returns 0 to cover idempotency.
Example addition after the successful withdraw:
vm.expectRevert(DisputeKitClassicBase.NothingToWithdraw.selector); disputeKit.withdrawFeesAndRewards(disputeID, payable(crowdfunder1), 1);Also applies to: 732-732, 740-746
contracts/test/foundry/KlerosCore_Appeals.t.sol (2)
301-317
: Robustness: also assert Active.dispute when checking currentRoundYou only assert currentRound; add a quick check for disputeActive to ensure the DK recognizes the core dispute at both old/new DKs.
- (, bool currentRound) = newDisputeKit.coreDisputeIDToActive(disputeID); - assertEq(currentRound, false, "round should be jumped"); + (bool disputeActive, bool currentRound) = newDisputeKit.coreDisputeIDToActive(disputeID); + assertEq(disputeActive, true, "dispute should exist in the previous DK"); + assertEq(currentRound, false, "round should be jumped");
439-455
: Same nit: assert both fields of ActiveMirrors the previous suggestion for consistency and clearer failures.
- (, bool currentRound) = disputeKit3.coreDisputeIDToActive(disputeID); - assertEq(currentRound, false, "round should be jumped"); + (bool disputeActive, bool currentRound) = disputeKit3.coreDisputeIDToActive(disputeID); + assertEq(disputeActive, true, "dispute should exist in the previous DK"); + assertEq(currentRound, false, "round should be jumped");- (, currentRound) = disputeKit2.coreDisputeIDToActive(disputeID); - assertEq(currentRound, true, "round should be active in the DK that dispute jumped to"); + (disputeActive, currentRound) = disputeKit2.coreDisputeIDToActive(disputeID); + assertEq(disputeActive, true, "dispute should exist in the new DK"); + assertEq(currentRound, true, "round should be active in the DK that dispute jumped to");contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol (4)
111-117
: Event signature change: confirm downstream consumersWithdrawal now omits coreRoundID. Ensure off-chain indexers/UI consuming the old 4-topic signature are updated, or emit a deprecated event for one release to ease transition.
Would you like a thin compatibility event emitted alongside the new one for a deprecation window?
138-142
: Modifier name vs semanticsisActive() now encodes both “known dispute” and “current round on this DK.” Clear. Consider renaming to requireCurrentRound() for readability, keeping isActive as a pure “known” check for public views.
372-436
: Appeal/jump flow is correct; minor gas and clarity tweaks
- External call this.currentRuling is more expensive than reading state; if not required for polymorphism, prefer internal helper.
- When marking a jump, only currentRound is flipped; dispute remains true—good.
- (uint256 ruling, , ) = this.currentRuling(_coreDisputeID); + (uint256 ruling, , ) = currentRuling(_coreDisputeID);Make currentRuling internal/virtual (and add a public wrapper) if polymorphism is needed.
588-606
: View functions can underflow on unknown disputesgetFundedChoices/areVotesAllCast and similar read disputes[coreDisputeIDToLocal[_coreDisputeID]] and access rounds[length-1]. If the dispute is unknown to this DK, length==0 underflows. Either:
- Add a lightweight known-dispute check (a.dispute) and return safe defaults, or
- Document that callers must use coreDisputeIDToActive first.
function getFundedChoices(uint256 _coreDisputeID) public view returns (uint256[] memory fundedChoices) { - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; + if (!coreDisputeIDToActive[_coreDisputeID].dispute) revert DisputeUnknownInThisDisputeKit(); + Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; ... }Apply similarly to other views that index the last round. As per coding guidelines
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol
(11 hunks)contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol
(2 hunks)contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol
(2 hunks)contracts/src/arbitration/interfaces/IDisputeKit.sol
(1 hunks)contracts/test/foundry/KlerosCore_Appeals.t.sol
(6 hunks)contracts/test/foundry/KlerosCore_Disputes.t.sol
(1 hunks)contracts/test/foundry/KlerosCore_Execution.t.sol
(2 hunks)contracts/test/foundry/KlerosCore_Voting.t.sol
(2 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-30T17:18:12.895Z
Learnt from: jaybuidl
PR: kleros/kleros-v2#2145
File: contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol:277-286
Timestamp: 2025-09-30T17:18:12.895Z
Learning: In DisputeKitClassicBase.sol's castCommit function, jurors are allowed to re-submit commits during the commit period. The implementation uses a commitCount variable to track only first-time commits (where commit == bytes32(0)) so that totalCommitted is not incremented when a juror updates their existing commit.
Applied to files:
contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol
contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
- GitHub Check: Redirect rules - kleros-v2-testnet
- GitHub Check: Redirect rules - kleros-v2-testnet
- GitHub Check: Header rules - kleros-v2-testnet
- GitHub Check: Header rules - kleros-v2-testnet
- GitHub Check: Pages changed - kleros-v2-testnet
- GitHub Check: Pages changed - kleros-v2-testnet
- GitHub Check: Redirect rules - kleros-v2-testnet-devtools
- GitHub Check: Redirect rules - kleros-v2-neo
- GitHub Check: Header rules - kleros-v2-testnet-devtools
- GitHub Check: Header rules - kleros-v2-neo
- GitHub Check: Pages changed - kleros-v2-testnet-devtools
- GitHub Check: Pages changed - kleros-v2-neo
- GitHub Check: hardhat-tests
- GitHub Check: Analyze (javascript)
🔇 Additional comments (8)
contracts/test/foundry/KlerosCore_Disputes.t.sol (1)
91-94
: LGTM: updated disputes() return shape.Tuple now matches (numberOfChoices, extraData). Assertions are correct.
contracts/test/foundry/KlerosCore_Voting.t.sol (2)
459-467
: API alignment looks goodSwitching to destructuring Active and the updated disputes() return shape matches the new DK interface. No issues spotted.
477-477
: Correct revert selector updateUsing DisputeUnknownInThisDisputeKit is consistent with the new isActive/unknown semantics.
contracts/test/foundry/KlerosCore_Appeals.t.sol (2)
470-775
: End-to-end recurring DK test is thoroughGreat coverage across DK2↔DK3 jumps, local/core IDs, funded choices, periods, draws, voting, and rewards. Event expectations align with the new flow and updated Withdrawal signature.
826-827
: Updated disputes() tuple desugaring LGTMThe reduced return arity is correctly handled here.
contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol (3)
200-231
: createDispute: safe round index and id reuse look good
- Reusing localDisputeID on re-entry across recurring DKs is correct.
- Mapping coreRoundIDToLocal to rounds.length pre-push is correct (points to the new round index).
Please confirm core calls createDispute after creating the new core round; otherwise core.getNumberOfRounds(_coreDisputeID) - 1 underflows.
631-636
: getJumpDisputeKitID fallback is sensibleReturning classic when jumpDisputeKitID is unset matches expectations for graceful degradation.
755-759
: Error renaming is coherent with semanticsDisputeJumpedToAnotherDisputeKit and DisputeUnknownInThisDisputeKit clarify causes. Ensure tests and docs updated (they seem to be).
/// @notice Allows those contributors who attempted to fund an appeal round to withdraw any reimbursable fees or rewards after the dispute gets resolved. | ||
/// @dev Withdrawals are not possible if the core contract is paused. | ||
/// @dev It can be called after the dispute has jumped to another dispute kit. | ||
/// @param _coreDisputeID Index of the dispute in Kleros Core contract. | ||
/// @param _beneficiary The address whose rewards to withdraw. | ||
/// @param _coreRoundID The round in the Kleros Core contract the caller wants to withdraw from. | ||
/// @param _choice The ruling option that the caller wants to withdraw from. | ||
/// @return amount The withdrawn amount. | ||
function withdrawFeesAndRewards( | ||
uint256 _coreDisputeID, | ||
address payable _beneficiary, | ||
uint256 _coreRoundID, | ||
uint256 _choice | ||
) external returns (uint256 amount) { | ||
(, , , bool isRuled, ) = core.disputes(_coreDisputeID); | ||
if (!isRuled) revert DisputeNotResolved(); | ||
if (core.paused()) revert CoreIsPaused(); | ||
if (!coreDisputeIDToActive[_coreDisputeID]) revert NotActiveForCoreDisputeID(); | ||
if (!coreDisputeIDToActive[_coreDisputeID].dispute) revert DisputeUnknownInThisDisputeKit(); | ||
|
||
Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; | ||
Round storage round = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]]; | ||
(uint256 finalRuling, , ) = core.currentRuling(_coreDisputeID); | ||
|
||
if (!round.hasPaid[_choice]) { | ||
// Allow to reimburse if funding was unsuccessful for this ruling option. | ||
amount = round.contributions[_beneficiary][_choice]; | ||
} else { | ||
// Funding was successful for this ruling option. | ||
if (_choice == finalRuling) { | ||
// This ruling option is the ultimate winner. | ||
amount = round.paidFees[_choice] > 0 | ||
? (round.contributions[_beneficiary][_choice] * round.feeRewards) / round.paidFees[_choice] | ||
: 0; | ||
} else if (!round.hasPaid[finalRuling]) { | ||
// The ultimate winner was not funded in this round. In this case funded ruling option(s) are reimbursed. | ||
amount = | ||
(round.contributions[_beneficiary][_choice] * round.feeRewards) / | ||
(round.paidFees[round.fundedChoices[0]] + round.paidFees[round.fundedChoices[1]]); | ||
for (uint256 i = 0; i < dispute.rounds.length; i++) { | ||
Round storage round = dispute.rounds[i]; | ||
|
||
if (!round.hasPaid[_choice]) { | ||
// Allow to reimburse if funding was unsuccessful for this ruling option. | ||
amount += round.contributions[_beneficiary][_choice]; | ||
} else { | ||
// Funding was successful for this ruling option. | ||
if (_choice == finalRuling) { | ||
// This ruling option is the ultimate winner. | ||
amount += round.paidFees[_choice] > 0 | ||
? (round.contributions[_beneficiary][_choice] * round.feeRewards) / round.paidFees[_choice] | ||
: 0; | ||
} else if (!round.hasPaid[finalRuling]) { | ||
// The ultimate winner was not funded in this round. In this case funded ruling option(s) are reimbursed. | ||
amount += | ||
(round.contributions[_beneficiary][_choice] * round.feeRewards) / | ||
(round.paidFees[round.fundedChoices[0]] + round.paidFees[round.fundedChoices[1]]); | ||
} | ||
} | ||
round.contributions[_beneficiary][_choice] = 0; | ||
} | ||
round.contributions[_beneficiary][_choice] = 0; | ||
|
||
if (amount != 0) { | ||
_beneficiary.safeSend(amount, wNative); | ||
emit Withdrawal(_coreDisputeID, _coreRoundID, _choice, _beneficiary, amount); | ||
emit Withdrawal(_coreDisputeID, _choice, _beneficiary, amount); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Withdrawals: guard unknown disputes and handle edge cases explicitly
- Good: allows withdrawals after jumps; iterates all rounds; zeroes contributions.
- Edge case: else-if branch assumes two funded choices; enforce fundedChoices.length == 2 to avoid accidental OOB or skew if rules ever change.
- } else if (!round.hasPaid[finalRuling]) {
+ } else if (!round.hasPaid[finalRuling]) {
+ // Expect exactly 2 funded choices in this design.
+ if (round.fundedChoices.length < 2) continue;
amount +=
(round.contributions[_beneficiary][_choice] * round.feeRewards) /
(round.paidFees[round.fundedChoices[0]] + round.paidFees[round.fundedChoices[1]]);
}
Also consider reverting with DisputeInconsistentFunding() if length != 2 to surface data inconsistencies early.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol around lines
438 to 485, the else-if branch assumes round.fundedChoices has exactly two
entries when summing paidFees by indexing [0] and [1]; add an explicit guard
that round.fundedChoices.length == 2 before accessing those indices and if not
true revert with DisputeInconsistentFunding() (or an existing appropriate error)
to surface inconsistent funding state; place this check immediately before the
division that uses fundedChoices and use the guarded branch to compute the
reimbursement, otherwise revert.
PR-Codex overview
This PR focuses on enhancing the
DisputeKitClassicBase
contract's handling of disputes by refining the active status checks and modifying the structure of disputes and rounds. It also improves error handling and updates related events and function signatures.Detailed summary
Active
to track dispute and round activity.jumped
boolean withActive
struct incoreDisputeIDToActive
.notJumped
toisActive
to check dispute status.Summary by CodeRabbit
Bug Fixes
Tests